<template>
    <div class="puzzle">
        <img crossorigin="" hidden :src="img" alt="" id="puzzle-img" />
        <canvas
            id="puzzle-background"
            :width="width"
            :height="height"
            style="background:transparent;"
        ></canvas>
        <canvas
            :style="slideOffsetComputed"
            width="50"
            height="50"
            id="puzzle-slide"
        ></canvas>
        <canvas
            :style="goalOffsetComputed"
            width="50"
            height="50"
            id="puzzle-goal"
        ></canvas>

        <!-- 

    @mousemove="onMouseMove"
            @mouseleave="onMouseLeave"
            @mouseup="onMouseUp"
 -->
        <div class="puzzle-box">
            <div class="puzzle-handle">
                <div
                    :style="puzzleButtonComputed"
                    class="puzzle-button"
                    @mousedown="onMouseDown"
                >
                    <svg
                        xmlns="http://www.w3.org/2000/svg"
                        xmlns:xlink="http://www.w3.org/1999/xlink"
                        viewBox="0 0 24 24"
                    >
                        <path
                            d="M10 17l5-5l-5-5v10z"
                            fill="currentColor"
                        ></path>
                    </svg>
                </div>
            </div>
        </div>
        <div :class="puzzleResultClassComputed">
            {{ puzzleResultTextComputed }}
        </div>
        <div class="puzzle-refresh" @click="reset">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                xmlns:xlink="http://www.w3.org/1999/xlink"
                viewBox="0 0 24 24"
            >
                <path
                    d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
                    fill="currentColor"
                ></path>
            </svg>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        width: {
            type: Number,
            default: 300,
        },
        height: {
            type: Number,
            default: 150,
        },

        offsetDistance: {
            type: Number,
            default: 10,
        },
        successText: {
            type: String,
            default: "校验成功",
        },
        failText: {
            type: String,
            default: "校验失败",
        },

        errorRetry: {
            type: Boolean,
            default: true,
        },

        img: {
            type: String,
            default: "https://i.loli.net/2021/08/30/lj1ciV3An8JDo6u.jpg",
        },
    },
    data() {
        return {
            backgroundContext: null,
            slideContext: null,
            goalContext: null,

            offsetX: 0,
            offsetY: 0,

            isMousedown: false,
            mouseClientX: 0,

            slideOffsetX: 10,
            distance: 0,

            result: "default",
        };
    },
    beforeDestroy() {
        this.removeMoveWatcher();
    },
    mounted() {
        this.initMoveWatcher();
        this.initCanvasContext();
        this.drawBackgroundImage();
    },

    computed: {
        goalOffsetComputed({ offsetX, offsetY }) {
            return {
                left: `${offsetX}px`,
                top: `${offsetY}px`,
            };
        },

        slideOffsetComputed({ distance, slideOffsetX, offsetY }) {
            return {
                left: `${slideOffsetX + distance + 10}px`,
                top: `${offsetY}px`,
            };
        },
        puzzleButtonComputed({ distance }) {
            return {
                left: `${distance}px`,
            };
        },

        puzzleResultClassComputed({ result }) {
            return [
                "puzzle-result",
                (result == "success" || result == "fail") &&
                    `puzzle-result-${result}`,
            ];
        },
        puzzleResultTextComputed({ result }) {
            const TEXT_ENMU = {
                success: this.successText,
                fail: this.failText,
            };
            return TEXT_ENMU[result];
        },
    },
    methods: {
        initMoveWatcher() {
            const body = document.body;
            body.addEventListener("mouseleave", this.onMouseLeave);
            body.addEventListener("mousemove", this.onMouseMove);
            body.addEventListener("mouseup", this.onMouseUp);
        },

        removeMoveWatcher() {
            const body = document.body;
            body.removeEventListener("mouseleave", this.onMouseLeave);
            body.removeEventListener("mousemove", this.onMouseMove);
            body.removeEventListener("mouseup", this.onMouseUp);
        },

        initCanvasContext() {
            this.backgroundContext = this.getCanvasContext(
                "#puzzle-background"
            );
            this.slideContext = this.getCanvasContext("#puzzle-slide");
            this.goalContext = this.getCanvasContext("#puzzle-goal");
        },

        getCanvasContext(selector) {
            const canvas = document.querySelector(selector);
            return canvas.getContext("2d");
        },

        async drawBackgroundImage() {
            const img = await this.getImage();
            if (img) {
                const scale = this.width / this.height;
                this.backgroundContext.drawImage(
                    img,
                    0,
                    0,
                    img.width,
                    img.width / scale,
                    0,
                    0,
                    this.width,
                    this.height
                );
                this.drawSlideImage();
            }
        },

        getImage() {
            return new Promise(resolve => {
                const img = document.getElementById("puzzle-img");
                img.onload = () => {
                    resolve(img || false);
                };
            });
        },

        getBackgroundRandomArea() {
            const x = ~~(Math.random() * 80 + 140);
            const y = ~~((this.height - 50) / 2);
            const imgData = this.backgroundContext.getImageData(x, y, 50, 50);
            this.offsetX = 10 + x;
            this.offsetY = 10 + y;
            return imgData;
        },

        drawSlideImage() {
            const imgData = this.getBackgroundRandomArea();

            this.slideContext.putImageData(imgData, 0, 0);
        },

        onMouseDown({ clientX }) {
            this.isMousedown = true;
            this.mouseClientX = clientX;
        },
        onMouseUp() {
            this.resultVerify();
            this.isMousedown = false;
            this.mouseClientX = null;
        },
        onMouseMove({ clientX }) {
            if (this.isMousedown) {
                if (clientX - this.mouseClientX > 0) {
                    this.distance = clientX - this.mouseClientX;
                }
            }
        },
        onMouseLeave() {
            this.onMouseUp();
        },

        /**
         * @description
         * GAP 代表SLIDE 距离 GOAL 的偏差,其中 offsetX是生成GOAL的style left距离,
         * 注意：是距离最外层父元素的距离而不是BACKGROUND的距离,减去10是因为BACKGROUND
         * 距离最外层父元素有10间距偏差,然后this.slideOffsetX + this.distance是SLIDE
         * 真正距离BACKGROUND的距离,因为拖动按钮是从BACKGROUND对齐的.
         * 再次设置isMousedown = false,是因为校验完成之后不允许再次拖动,应该手动调用reset方法恢复出厂设置重新进行验证;
         */
        resultVerify() {
            if (this.isMousedown) {
                const GAP = Math.abs(
                    this.offsetX - 10 - (this.slideOffsetX + this.distance)
                );
                this.result = GAP <= this.offsetDistance ? "success" : "fail";
                this.isMousedown = false;
                if (this.errorRetry && this.result == "fail") {
                    setTimeout(this.reset, 1000);
                }
                this.$emit("complete", this.result);
            }
        },

        reset() {
            this.resetState();
            this.drawSlideImage();
            this.drawBackgroundImage();
        },
        resetState() {
            this.isMousedown = false;
            this.mouseClientX = 0;
            this.distance = 0;
            this.result = "default";
        },
    },
};
</script>

<style scoped>
.puzzle {
    padding: 10px;
    display: inline-block;
    background-color: #fff;
    border: 1px solid #f1f1f1;
    position: relative;
}

#puzzle-background {
}
#puzzle-slide {
    position: absolute;
    z-index: 5;
    background-color: transparent;
    box-shadow: 0 0 4px 0.8px rgba(255, 255, 255, 0.3);
}
#puzzle-goal {
    position: absolute;
    background-color: #fff;
    bottom: 50px;
    box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.3) inset;
}

.puzzle-box {
    height: 40px;
    margin-top: 7px;
    border: 1px solid #f1f1f1;
    position: relative;
    user-select: none;
}

.puzzle-handle {
    position: absolute;
}

.puzzle-button {
    position: absolute;
    height: 40px;
    width: 40px;
    background-color: #fff;
    box-sizing: border-box;
    box-shadow: 0 0 2px 1px lightgray;
    text-align: center;
    line-height: 40px;
    font-size: 14px;
    cursor: w-resize;
    color: #555;
    font-weight: bold;
}

.puzzle-button:hover {
    color: #000;
    font-weight: bold;
}

.puzzle-result {
    width: calc(100% - 20px);
    position: absolute;
    height: 0px;
    z-index: 6;
    bottom: 60px;
    transition: height 0.2s ease-out;
    text-align: center;
    color: #fff;
    font-size: 12px;
    font-family: 微软雅黑;
}

.puzzle-result-success {
    background-color: rgba(52, 189, 104, 1);
    height: 20px;
    line-height: 20px;
}
.puzzle-result-fail {
    background-color: #f13a3a;
    height: 20px;
    line-height: 20px;
}

.puzzle-refresh {
    position: absolute;
    right: 20px;
    top: 20px;
    width: 20px;
    height: 20px;
    cursor: pointer;
    background: #fff;
    border-radius: 50%;
}
</style>
